mealie, actualbudget: add native "Sign in with Authelia" (OIDC)

Researched which of the "has built-in auth" services actually support
native OIDC before wiring anything in (checked live docs, not assumed) —
two services turned out to contradict general assumption: Portainer's
OAuth/OIDC is Business Edition only (this repo installs portainer-ce, which
doesn't have it), and ntfy has no auth-oauth2-* support at all despite it
seeming like the kind of thing a modern self-hosted tool would have added
by now. Full findings recorded in CLAUDE.md so this doesn't need
re-researching.

Two real, verified wins wired up, both entirely env-var driven — no
manual config file editing, matching this repo's "no manual wizard"
philosophy and reusing the exact _authelia_provision_oidc_client /
_authelia_scope_access machinery already built for Gitea:

- mealie: OIDC_AUTH_ENABLED/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/
  OIDC_CONFIGURATION_URL appended to the existing .env (env_file: .env is
  already how mealie.sh's compose reads it). Also adds a
  --forwarded-allow-ips entrypoint override when Caddy-fronted — confirmed
  against Mealie's own issue tracker that without it, the generated OIDC
  redirect URI comes out http:// even when actually served over https://,
  which providers reject as a scheme mismatch.
- actualbudget: ACTUAL_OPENID_DISCOVERY_URL/CLIENT_ID/CLIENT_SECRET/
  SERVER_HOSTNAME, same pattern. Redirect path (/openid/callback) matches
  the preset already used by authelia.sh's own "Register an app" menu for
  this same service.

Both offered on fresh installs and Update reruns, default no, and both
call _authelia_scope_access() afterward so access can be restricted to
specific users instead of every Authelia user, same as Gitea.

Immich has real OIDC + a system-config API but needs one more
verification pass on the exact request payload before automating — not
guessing that part. Jellyfin and Home Assistant only have third-party
plugin/HACS-based OIDC, a bigger lift than an env-var toggle — noted but
not attempted this pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
This commit is contained in:
Claude
2026-08-19 18:44:00 +00:00
parent ab6b554cd8
commit 3dca8dce1d
3 changed files with 176 additions and 11 deletions
+24 -11
View File
@@ -416,18 +416,31 @@ universal by removing them from their `-only` group(s) — a pure users.yml
edit, since universal access is just the *absence* of a restricting group, edit, since universal access is just the *absence* of a restricting group,
not a rule of its own. not a rule of its own.
Only `services/gitea.sh` calls `_authelia_scope_access()` so far (the `services/gitea.sh`, `services/mealie.sh`, and `services/actualbudget.sh`
reference integration). The other services that already offer a plain call `_authelia_scope_access()` so far. The other services that already
"Protect X with Authelia SSO?" prompt (`magicmirror`, `wolf-pair`, offer a plain "Protect X with Authelia SSO?" prompt (`magicmirror`,
`js99er`, `drum-rhythm-game`, `iopaint`, `paintplus`, `stirling-pdf`, `wolf-pair`, `js99er`, `drum-rhythm-game`, `iopaint`, `paintplus`,
`wolf`) are natural, mechanical follow-ups — each just needs one added `stirling-pdf`, `wolf`) are natural, mechanical follow-ups — each just
call to `_authelia_scope_access` after its existing needs one added call to `_authelia_scope_access` after its existing
`configure_caddy_for_service` step, once Gitea's integration has been `configure_caddy_for_service` step, once Gitea's integration has been
confirmed working live. Extending *native* OIDC support (the pattern confirmed working live.
above, not just scoping) to the "has built-in auth" services beyond Gitea
needs verifying per service first — not every app in that list actually **Native OIDC support across the "has built-in auth" list — checked
has its own OAuth2/OIDC provider field, so don't assume one exists without against each app's real docs (2026-08), not assumed.** Don't extend this
checking that service's real settings. pattern to a service without checking its own current settings first —
two of the ones below turned out to need actual verification to get
right (Portainer, ntfy), not general familiarity with the product:
| Service | Native OIDC? | Notes |
|---|---|---|
| `mealie` | Yes — wired up | Pure env vars (`OIDC_AUTH_ENABLED`, `OIDC_CLIENT_ID/SECRET`, `OIDC_CONFIGURATION_URL`), see `_mealie_offer_authelia_oidc()`. Redirect URI is `<BASE_URL>/login`. Needs a `--forwarded-allow-ips` entrypoint override when Caddy-fronted, or the generated redirect URI comes out `http://` even when actually served over `https://` — see the function's own comment. |
| `actualbudget` | Yes — wired up | Pure env vars (`ACTUAL_OPENID_DISCOVERY_URL`, `ACTUAL_OPENID_CLIENT_ID/SECRET`, `ACTUAL_OPENID_SERVER_HOSTNAME`), see `_actualbudget_offer_authelia_oidc()`. Redirect path `/openid/callback` (matches the existing preset in `_authelia_add_oidc_client()`'s menu). First OIDC login becomes the server owner if none is set yet — Actual's own behavior. |
| `immich` | Yes, not yet wired up | Real OAuth2/OIDC settings under Administration → Settings, backed by a `system-config` API (GET/PUT) — confirmed the API exists, but didn't confirm the exact request payload shape needed to set OAuth fields specifically. Needs one more verification pass against the live OpenAPI spec before automating; don't guess the payload. |
| `jellyfin` | Only via a third-party plugin | No official native OIDC. Community plugins exist (`jellyfin-plugin-sso`, `jellyfin-plugin-oidc`) but are web-UI-only — native mobile/desktop Jellyfin clients can't use them. A bigger lift than an env-var toggle (plugin install via Jellyfin's own plugin repo system); hold off until that's worth doing deliberately. |
| `homeassistant` | Only via a third-party HACS integration | No native core OIDC as of 2026 (open community discussion asking for it, not shipped). `hass-oidc-auth`/`hass-openid` exist as HACS-installed integrations — same "bigger lift" caveat as Jellyfin. |
| `portainer` | No (CE) | OAuth/OIDC is a **Business Edition** feature — this repo installs `portainer-ce` (confirmed in `services/portainer.sh`), which doesn't have it. CE's documented path is fronting it with `oauth2-proxy`, i.e. no different from the forward_auth pattern any no-built-in-auth service already uses — not "native OIDC" in the sense this section means. |
| `ntfy` | No | Checked ntfy's own config docs directly — no `auth-oauth2-*` keys exist. Only basic auth + access tokens + ACLs. (Worth a re-check on a future ntfy release if this matters to you — this class of feature does get added to self-hosted tools over time.) |
| `emby`, `audiobookshelf`, `meshcentral`, `traccar`, `uptimekuma`, `filebrowser`, `wg-easy` | Not individually re-verified | High-confidence no, based on general familiarity with each product rather than a fresh doc check this pass (unlike everything above, which was actually checked and in two cases contradicted assumption). Verify before wiring any of these in, the same way the checked ones were — don't extrapolate from this table's pattern.
**No built-in auth — should be protected:** **No built-in auth — should be protected:**
`magicmirror`, `wolf-pair`, `js99er`, `drum-rhythm-game`, `iopaint`, `magicmirror`, `wolf-pair`, `js99er`, `drum-rhythm-game`, `iopaint`,
+71
View File
@@ -198,6 +198,73 @@ fi
register_service actualbudget utilities "Open-source personal finance & budgeting (Actual Budget)" 5006 register_service actualbudget utilities "Open-source personal finance & budgeting (Actual Budget)" 5006
# Offers to add "Sign in with Authelia" (OpenID Connect) to Actual Budget's
# own login page — same additive pattern as services/gitea.sh's
# _gitea_offer_authelia_sso, entirely environment-variable driven like
# services/mealie.sh's equivalent. Confirmed against Actual Budget's own
# OIDC docs: ACTUAL_OPENID_DISCOVERY_URL, ACTUAL_OPENID_CLIENT_ID,
# ACTUAL_OPENID_CLIENT_SECRET, ACTUAL_OPENID_SERVER_HOSTNAME, appended
# into the .env file this installer already writes and reads via
# `env_file: .env`. Redirect path (/openid/callback) matches the preset
# already used by services/authelia.sh's own "Register an app" menu for
# this same app, so both stay consistent with each other.
#
# No stored BASE_URL to read back here (unlike Mealie) — Actual Budget's
# compose/.env never records the public URL, so this asks for the domain
# directly instead, same as services/gitea.sh's SSO offer does.
#
# Args: DIR
_actualbudget_offer_authelia_oidc() {
local DIR="$1"
[ -d "$DOCKER_DIR/authelia" ] || return 0
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
grep -q '^ACTUAL_OPENID_DISCOVERY_URL=' "$DIR/.env" 2>/dev/null && return 0
echo ""
local USE_SSO=""
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Actual Budget's login page? (y/n):" "n" USE_SSO
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
local _default_domain=""
[ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ] && _default_domain="budget.${SITE_DOMAIN}"
local AB_OIDC_DOMAIN=""
prompt_text " Domain Actual Budget is reachable at [${_default_domain:-required}]:" "$_default_domain" AB_OIDC_DOMAIN
if [ -z "$AB_OIDC_DOMAIN" ]; then
log_warning "No domain entered — skipping Authelia SSO for Actual Budget."
return 0
fi
local _2fa="" AUTH_POLICY="two_factor"
prompt_yn " Require two-factor for Actual Budget logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "ActualBudget" "actualbudget" "$AUTH_POLICY" "y" \
"https://${AB_OIDC_DOMAIN}/openid/callback"; then
log_warning "Couldn't register Actual Budget as an OIDC client in Authelia — skipping SSO setup."
return 0
fi
local _discovery_url="https://auth.${OIDC_AUTHELIA_DOMAIN}/.well-known/openid-configuration"
cat >> "$DIR/.env" << ENV
# Written by services/actualbudget.sh's Authelia SSO step. The first OIDC
# login becomes the Actual Budget server owner if no owner is set yet —
# that's Actual Budget's own behavior, not something this script controls.
ACTUAL_OPENID_DISCOVERY_URL=$_discovery_url
ACTUAL_OPENID_CLIENT_ID=actualbudget
ACTUAL_OPENID_CLIENT_SECRET=$OIDC_CLIENT_SECRET_PLAIN
ACTUAL_OPENID_SERVER_HOSTNAME=https://${AB_OIDC_DOMAIN}
ENV
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true
(cd "$DIR" && docker compose up -d) \
&& log_success "\"Sign in with Authelia\" added to Actual Budget — local login still works too." \
|| log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "actualbudget" "$AB_OIDC_DOMAIN"
}
install_actualbudget() { install_actualbudget() {
require_docker || return 1 require_docker || return 1
@@ -216,6 +283,7 @@ install_actualbudget() {
echo " - Create \$DOCKER_DIR/actualbudget(-<name>) with docker-compose.yml (data/)" echo " - Create \$DOCKER_DIR/actualbudget(-<name>) with docker-compose.yml (data/)"
echo " - Auto-scan for a free host port if this is an additional instance" echo " - Auto-scan for a free host port if this is an additional instance"
echo " - Offer a Caddy reverse proxy and to start the container" echo " - Offer a Caddy reverse proxy and to start the container"
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
return 0 return 0
fi fi
@@ -258,6 +326,7 @@ install_actualbudget() {
( cd "$AB_DIR" && docker compose pull && docker compose up -d ) \ ( cd "$AB_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Actual Budget image refreshed" \ && log_success "Actual Budget image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $AB_DIR/docker-compose.yml logs" || log_warning "Refresh failed — check: docker compose -f $AB_DIR/docker-compose.yml logs"
declare -F _actualbudget_offer_authelia_oidc >/dev/null 2>&1 && _actualbudget_offer_authelia_oidc "$AB_DIR"
return 0 return 0
;; ;;
cancel) cancel)
@@ -333,6 +402,8 @@ AB_ENV
configure_caddy_for_service "ActualBudget${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:5006" "budget${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}" configure_caddy_for_service "ActualBudget${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:5006" "budget${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
declare -F _actualbudget_offer_authelia_oidc >/dev/null 2>&1 && _actualbudget_offer_authelia_oidc "$AB_DIR"
write_readme "$AB_DIR" << MD write_readme "$AB_DIR" << MD
# Actual Budget${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX} # Actual Budget${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
+81
View File
@@ -198,6 +198,83 @@ fi
register_service mealie utilities "Recipe manager & meal planner (Mealie)" 9925 register_service mealie utilities "Recipe manager & meal planner (Mealie)" 9925
# Offers to add "Sign in with Authelia" (OpenID Connect) to Mealie's own
# login page — same additive pattern as services/gitea.sh's
# _gitea_offer_authelia_sso (local login keeps working unchanged), but
# Mealie's OIDC support is entirely environment-variable driven — no CLI
# equivalent to Gitea's `admin auth add-oauth` needed. Confirmed against
# Mealie's own OIDC docs: OIDC_AUTH_ENABLED, OIDC_CLIENT_ID,
# OIDC_CLIENT_SECRET, OIDC_CONFIGURATION_URL, OIDC_SIGNUP_ENABLED, appended
# straight into the .env file this installer already writes and reads via
# `env_file: .env` — no docker-compose.yml regeneration needed for that part.
#
# Reads BASE_URL back from the existing .env rather than taking it as an
# arg, so this works identically whether called right after a fresh
# install (where the URL was just computed) or from an Update rerun
# (where it wasn't recomputed this run, but is already on disk).
#
# Args: DIR CONTAINER
_mealie_offer_authelia_oidc() {
local DIR="$1" CONTAINER="$2"
[ -d "$DOCKER_DIR/authelia" ] || return 0
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
grep -q '^OIDC_AUTH_ENABLED=' "$DIR/.env" 2>/dev/null && return 0
local BASE_URL
BASE_URL="$(grep '^BASE_URL=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)"
if [ -z "$BASE_URL" ]; then
log_warning "Couldn't find BASE_URL in $DIR/.env — skipping Authelia SSO offer for Mealie."
return 0
fi
echo ""
local USE_SSO=""
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Mealie's login page? (y/n):" "n" USE_SSO
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
local _2fa="" AUTH_POLICY="two_factor"
prompt_yn " Require two-factor for Mealie logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "Mealie" "mealie" "$AUTH_POLICY" "y" "${BASE_URL}/login"; then
log_warning "Couldn't register Mealie as an OIDC client in Authelia — skipping SSO setup."
return 0
fi
local _discovery_url="https://auth.${OIDC_AUTHELIA_DOMAIN}/.well-known/openid-configuration"
cat >> "$DIR/.env" << ENV
# Written by services/mealie.sh's Authelia SSO step — adds "Sign in with
# Authelia" alongside local login; local accounts keep working unchanged.
OIDC_AUTH_ENABLED=true
OIDC_SIGNUP_ENABLED=true
OIDC_CLIENT_ID=mealie
OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET_PLAIN
OIDC_CONFIGURATION_URL=$_discovery_url
OIDC_PROVIDER_NAME=Authelia
ENV
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true
# Mealie's OIDC redirect URI generation trusts X-Forwarded-* only from
# explicitly allowed IPs — without this, a Caddy-fronted instance
# generates an http:// redirect URI even when actually served over
# https://, which Authelia/any OIDC provider rejects as a scheme
# mismatch. Confirmed against Mealie's own reverse-proxy docs/issue
# tracker. Only needed (and only added) when Caddy is actually
# fronting this instance — BASE_URL itself tells us that (it's only
# ever https:// when a real domain + Caddy were configured).
if [[ "$BASE_URL" == https://* ]] && ! grep -q '^ entrypoint:' "$DIR/docker-compose.yml"; then
sed -i "/container_name: ${CONTAINER}\$/a\\ entrypoint: [\"uvicorn\", \"mealie.app:app\", \"--host\", \"0.0.0.0\", \"--port\", \"9000\", \"--forwarded-allow-ips=*\"]" "$DIR/docker-compose.yml"
fi
(cd "$DIR" && docker compose up -d) \
&& log_success "\"Sign in with Authelia\" added to Mealie — local login still works too." \
|| log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "mealie" "${BASE_URL#*://}"
}
install_mealie() { install_mealie() {
require_docker || return 1 require_docker || return 1
@@ -217,6 +294,7 @@ install_mealie() {
echo " - Auto-scan for a free host port if this is an additional instance" echo " - Auto-scan for a free host port if this is an additional instance"
echo " - Default login: changeme@email.com / MyPassword (change immediately)" echo " - Default login: changeme@email.com / MyPassword (change immediately)"
echo " - Offer a Caddy reverse proxy and to start the container" echo " - Offer a Caddy reverse proxy and to start the container"
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
return 0 return 0
fi fi
@@ -259,6 +337,7 @@ install_mealie() {
( cd "$MEALIE_DIR" && docker compose pull && docker compose up -d ) \ ( cd "$MEALIE_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Mealie image refreshed" \ && log_success "Mealie image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $MEALIE_DIR/docker-compose.yml logs" || log_warning "Refresh failed — check: docker compose -f $MEALIE_DIR/docker-compose.yml logs"
declare -F _mealie_offer_authelia_oidc >/dev/null 2>&1 && _mealie_offer_authelia_oidc "$MEALIE_DIR" "$CONTAINER"
return 0 return 0
;; ;;
cancel) cancel)
@@ -352,6 +431,8 @@ MEALIE_ENV
configure_caddy_for_service "Mealie${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:9000" "recipes${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}" configure_caddy_for_service "Mealie${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:9000" "recipes${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
declare -F _mealie_offer_authelia_oidc >/dev/null 2>&1 && _mealie_offer_authelia_oidc "$MEALIE_DIR" "$CONTAINER"
write_readme "$MEALIE_DIR" << MD write_readme "$MEALIE_DIR" << MD
# Mealie${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX} # Mealie${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}