Merge pull request #360 from outis1one/claude/gitea-standalone-setup-oxoi2e

Claude/gitea standalone setup oxoi2e
This commit is contained in:
Outis
2026-08-20 11:40:40 -04:00
committed by GitHub
5 changed files with 590 additions and 11 deletions
+71
View File
@@ -198,6 +198,73 @@ fi
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() {
require_docker || return 1
@@ -216,6 +283,7 @@ install_actualbudget() {
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 " - Offer a Caddy reverse proxy and to start the container"
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
return 0
fi
@@ -258,6 +326,7 @@ install_actualbudget() {
( cd "$AB_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Actual Budget image refreshed" \
|| 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
;;
cancel)
@@ -333,6 +402,8 @@ AB_ENV
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
# Actual Budget${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
+356 -6
View File
@@ -232,10 +232,12 @@ install_authelia() {
echo " Vaultwarden, or any other app with its own \"Enable OpenID\" setting)"
echo " 5) Reconfigure from scratch (regenerates secrets/users — breaks"
echo " existing sessions for every domain already on this instance)"
echo " 6) Leave as-is"
echo " 6) Show who has universal vs. service-scoped access"
echo " 7) Change \"Remember me\" session duration (stay logged in longer)"
echo " 8) Leave as-is"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1/2/3/4/5/6]:" "6" EXISTING_CHOICE
prompt_text " Choice [1/2/3/4/5/6/7/8]:" "8" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -256,6 +258,14 @@ install_authelia() {
5)
: # fall through to the full reinstall flow below
;;
6)
_authelia_report_access_scope
return 0
;;
7)
_authelia_set_remember_me
return 0
;;
*)
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
return 0
@@ -915,6 +925,310 @@ _authelia_toggle_admin() {
fi
}
# Same shape as _authelia_toggle_admin but for an arbitrary group name —
# used to scope a user's access to a single service (see
# _authelia_scope_access below) rather than the fixed "admins" group.
_authelia_toggle_group() {
local users_file="$1" start="$2" end="$3" group="$4" enable="$5"
if [ "$enable" = "true" ]; then
if ! sed -n "${start},${end}p" "$users_file" | grep -qF " - ${group}"; then
awk -v s="$start" -v e="$end" -v grp=" - ${group}" '
{ print }
NR>=s && NR<=e && /^ groups:$/ { print grp }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
fi
else
awk -v s="$start" -v e="$end" -v grpline=" - ${group}" '
NR>=s && NR<=e && $0==grpline { next }
{ print }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
fi
}
# Non-interactive core of add_authelia_user() below — no prompts, takes
# everything as args, generates a temp password + hash, and writes the user
# block directly into an arbitrary extra group (not just "users"). Used by
# _authelia_scope_access() to create users on the fly when someone lists a
# username that doesn't exist yet. Deliberately a separate function rather
# than a refactor of add_authelia_user() itself — that one's already in
# regular use via the interactive menu and this repo's convention is to
# extract a non-interactive core only when a second caller actually needs
# it (see _authelia_provision_oidc_client for the same reasoning), which
# keeps this addition low-risk to the existing, working function.
#
# Args: USERNAME DISPLAY EMAIL GROUP
# Out-param (not `local`): AUTHELIA_NEW_USER_TEMP_PASSWORD
# Returns 1 if the user already exists or hash generation fails.
_authelia_create_user_noninteractive() {
local username="$1" display="$2" email="$3" group="$4"
local users_file="$DOCKER_DIR/authelia/config/users.yml"
AUTHELIA_NEW_USER_TEMP_PASSWORD=""
if grep -qE "^ ${username}:$" "$users_file" 2>/dev/null; then
log_warning "'$username' already exists in $users_file."
return 1
fi
local temp_pass new_hash
temp_pass="$(_authelia_gen_temp_password)"
new_hash=$(docker run --rm authelia/authelia:4.39.20 \
authelia crypto hash generate argon2 --password "$temp_pass" 2>/dev/null \
| grep -oP '(?<=Digest: ).*')
if [ -z "$new_hash" ]; then
log_warning "Couldn't generate a password hash for '$username' automatically."
return 1
fi
local user_block=" ${username}:
displayname: \"${display}\"
email: ${email}
password: \"${new_hash}\"
groups:
- ${group}"
awk -v block="$user_block" '
{ print }
/^users:$/ && !done { print block; done=1 }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
chown 1000:1000 "$users_file" 2>/dev/null || true
AUTHELIA_NEW_USER_TEMP_PASSWORD="$temp_pass"
log_success "Created user '$username' (group: $group)"
return 0
}
# Reusable by ANY service, after it's already been protected by Authelia —
# forward_auth gate or native OIDC alike, since this only cares about the
# domain, not the gating mechanism. Asks whether access to $DOMAIN should be
# open to any Authelia user (today's only behavior, before this existed) or
# scoped to a specific list. If scoped: creates a dedicated group named
# "<service_id>-only", adds every listed username to it (creating any that
# don't exist yet via _authelia_create_user_noninteractive), and inserts two
# access_control rules ABOVE the general catch-all — allow this group on
# $DOMAIN, deny this group on every other protected domain on the instance —
# so members can reach ONLY this one domain. Idempotent: reruns against a
# domain that's already scoped just report the existing group instead of
# duplicating rules.
#
# Args: SERVICE_ID DOMAIN
_authelia_scope_access() {
local service_id="$1" domain="$2"
local authelia_dir="$DOCKER_DIR/authelia"
local config_file="$authelia_dir/config/configuration.yml"
local users_file="$authelia_dir/config/users.yml"
[ -f "$config_file" ] || return 0
local group="${service_id}-only"
if grep -qF "subject: \"group:${group}\"" "$config_file" 2>/dev/null; then
log_info "Access to $domain is already scoped to group '$group'."
log_info "Manage its members via this menu's \"Edit an existing user\" (toggle their groups by hand in users.yml), or the universal-access report below."
return 0
fi
echo ""
echo " Who should be able to reach $domain via Authelia?"
echo " 1) Any Authelia user (default — same access as everything else)"
echo " 2) Specific users only"
local scope_choice=""
prompt_text " Choice [1/2]:" "1" scope_choice
[ "$scope_choice" = "2" ] || return 0
echo " Usernames who should have access (space-separated). Anyone listed"
echo " who doesn't already have an Authelia account gets one created —"
echo " you'll get their temporary password to hand over."
local raw_users=""
prompt_text " Usernames:" "" raw_users
local -a usernames
read -ra usernames <<< "$raw_users"
if [ "${#usernames[@]}" -eq 0 ]; then
log_warning "No usernames entered — leaving $domain open to all Authelia users."
return 0
fi
local u start_end start end
for u in "${usernames[@]}"; do
u="$(echo "$u" | tr -cs 'a-z0-9_-' '-' | sed 's/^-*//;s/-*$//')"
[ -z "$u" ] && continue
if grep -qE "^ ${u}:$" "$users_file" 2>/dev/null; then
start_end="$(_authelia_user_line_range "$users_file" "$u")"
start="${start_end% *}"; end="${start_end#* }"
_authelia_toggle_group "$users_file" "$start" "$end" "$group" "true"
log_success "Added '$u' to group '$group'"
else
local email_default="${u}@${SITE_DOMAIN:-example.com}"
if _authelia_create_user_noninteractive "$u" "$u" "$email_default" "$group"; then
echo " Temp password for '$u': $AUTHELIA_NEW_USER_TEMP_PASSWORD"
fi
fi
done
# Two rules, both above the general catch-all: allow this group on the
# target domain, deny this group on every other protected domain. Order
# matters — Authelia takes the first matching rule, so both must land
# before access_control's existing "*.${AUTHELIA_DOMAIN}" catch-all.
local authelia_domain
authelia_domain="$(awk '/^ cookies:$/{f=1; next} f && /domain:/{print $3; exit}' "$config_file")"
local scope_rules=" - domain: \"${domain}\"
subject: \"group:${group}\"
policy: two_factor
- domain: \"*.${authelia_domain}\"
subject: \"group:${group}\"
policy: deny"
awk -v block="$scope_rules" '
/^ rules:$/ && !done { print; print block; done=1; next }
{ print }
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
chown 1000:1000 "$config_file" 2>/dev/null || true
local restart_auth=""
prompt_yn " Restart Authelia to apply this scoping? (y/n):" "y" restart_auth
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
(cd "$authelia_dir" && docker compose restart authelia 2>/dev/null) \
&& log_success "Authelia restarted — $domain is now restricted to group '$group'." \
|| log_warning "Restart failed — check: docker compose logs authelia"
fi
}
# Reporting/management: lists which users have "universal" access (every
# protected domain — anyone not locked into a "<service>-only" group) versus
# which are scoped to specific services, then offers to promote a scoped
# user to universal by removing them from all their "-only" groups. Doesn't
# touch access_control.rules at all — universal access is just the absence
# of a restricting group, so "promoting" someone is purely a users.yml edit.
_authelia_report_access_scope() {
local users_file="$DOCKER_DIR/authelia/config/users.yml"
[ -f "$users_file" ] || { log_warning "No users.yml found — install Authelia first."; return 1; }
local -a all_users
mapfile -t all_users < <(_authelia_list_usernames "$users_file")
if [ "${#all_users[@]}" -eq 0 ]; then
log_warning "No users found in $users_file."
return 0
fi
echo ""
echo " Universal access (every protected domain):"
local -a universal=() restricted=()
local u start_end start end groups_in_range
for u in "${all_users[@]}"; do
start_end="$(_authelia_user_line_range "$users_file" "$u")"
start="${start_end% *}"; end="${start_end#* }"
groups_in_range="$(sed -n "${start},${end}p" "$users_file" | grep -oE '\- [a-z0-9_-]+-only$' | sed 's/^- //')"
if [ -z "$groups_in_range" ]; then
universal+=("$u")
echo " - $u"
else
restricted+=("$u ($(echo "$groups_in_range" | tr '\n' ',' | sed 's/,$//'))")
fi
done
[ "${#universal[@]}" -eq 0 ] && echo " (none)"
echo ""
echo " Scoped to specific services only:"
if [ "${#restricted[@]}" -eq 0 ]; then
echo " (none)"
else
printf ' - %s\n' "${restricted[@]}"
fi
echo ""
local promote=""
prompt_yn " Promote a scoped user to universal access? (y/n):" "n" promote
[[ "$promote" =~ ^[Yy]$ ]] || return 0
local target=""
prompt_text " Username to promote:" "" target
[ -z "$target" ] && return 0
if ! grep -qE "^ ${target}:$" "$users_file" 2>/dev/null; then
log_warning "'$target' not found in $users_file."
return 0
fi
start_end="$(_authelia_user_line_range "$users_file" "$target")"
start="${start_end% *}"; end="${start_end#* }"
local -a target_groups
mapfile -t target_groups < <(sed -n "${start},${end}p" "$users_file" | grep -oE '\- [a-z0-9_-]+-only$' | sed 's/^- //')
if [ "${#target_groups[@]}" -eq 0 ]; then
log_info "'$target' already has universal access."
return 0
fi
local g
for g in "${target_groups[@]}"; do
_authelia_toggle_group "$users_file" "$start" "$end" "$g" "false"
done
log_success "'$target' removed from: ${target_groups[*]} — now has universal access."
local restart_auth=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
&& log_success "Authelia restarted" \
|| log_warning "Restart failed — check: docker compose logs authelia"
fi
}
# Changes how long an Authelia session lasts when a user checks "Remember
# me" at login — the actual mechanism behind "log in once, don't get asked
# again for a long time" for every domain this instance protects.
#
# The config key is `remember_me` (plain, under session:), NOT
# `remember_me_duration` — that name was retired in Authelia 4.38, this
# repo pins 4.39.20. Confirmed against Authelia's own docs/changelog
# before writing this; an easy mistake since older guidance (including an
# earlier version of this very file's own README section) uses the old
# name, which Authelia would just silently ignore rather than error on.
#
# This only controls AUTHELIA's own session — it does not touch how long
# a native-OIDC app's (Gitea/Mealie/ActualBudget) own session/token lasts
# after logging in via Authelia. A long remember_me makes re-authenticating
# to Authelia itself instant/silent whenever one of those apps' own
# session expires and sends you back through the OIDC flow, but doesn't
# stop that app's own session from expiring on its own separate schedule.
_authelia_set_remember_me() {
local config_file="$DOCKER_DIR/authelia/config/configuration.yml"
[ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; }
local current
current="$(grep -E '^ remember_me:' "$config_file" | awk '{print $2}' | tr -d "'\"")"
echo ""
echo " Current \"remember me\" duration: ${current:-not set}"
echo " How long a session lasts when someone checks \"Remember me\" at login —"
echo " applies to every domain this Authelia instance protects."
echo " Examples: 12h, 7d, 1M (month), 1y. Set to -1 to disable Remember Me entirely."
local new_duration=""
prompt_text " New duration [${current:-7d}]:" "${current:-7d}" new_duration
if [ -z "$new_duration" ] || [ "$new_duration" = "$current" ]; then
log_info "No change made."
return 0
fi
if grep -qE '^ remember_me:' "$config_file"; then
sed -i "s/^ remember_me:.*/ remember_me: '${new_duration}'/" "$config_file"
else
sed -i "/^session:\$/a\\ remember_me: '${new_duration}'" "$config_file"
fi
chown 1000:1000 "$config_file" 2>/dev/null || true
log_success "\"Remember me\" duration set to ${new_duration}."
local restart_auth=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
&& log_success "Authelia restarted" \
|| log_warning "Restart failed — check: docker compose logs authelia"
fi
echo ""
log_info "Takes effect for NEW logins where \"Remember me\" is checked at Authelia's"
log_info "login page — existing sessions keep whatever expiration they already had."
log_info "The checkbox itself is already on the login form by default; this only"
log_info "changes how long checking it actually keeps you signed in."
}
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
# configuration.yml (handles multi-domain instances from add_authelia_domain
@@ -1190,6 +1504,30 @@ _authelia_ensure_oidc_provider() {
log_success "OIDC provider enabled (signing key + HMAC secret generated)"
}
# Deletes one OIDC client block (matched by client_id) from
# identity_providers.oidc.clients in configuration.yml. Used by
# _authelia_provision_oidc_client below to make re-registering a client_id
# idempotent instead of a dead end — see that function's own comment on
# why a stale registration is safe to just replace. A client block starts
# at its own " - client_id: '<id>'" line (6-space indent) and runs
# until either the next such line or a line indented less than 6 spaces
# (end of the clients list) — deleting stops exactly there so a sibling
# client's block, or whatever config section follows, is untouched.
_authelia_remove_oidc_client() {
local config_file="$1" client_id="$2"
awk -v target="'${client_id}'" '
{
if ($0 ~ /^ - client_id: /) {
skip = ($0 ~ target) ? 1 : 0
} else if (skip && $0 !~ /^ /) {
skip = 0
}
if (!skip) print
}
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
chown 1000:1000 "$config_file" 2>/dev/null || true
}
# Non-interactive core of _authelia_add_oidc_client() below — generates a
# client secret, patches it into identity_providers.oidc.clients, and
# (optionally) restarts Authelia. Fully self-contained (re-validates
@@ -1207,8 +1545,10 @@ _authelia_ensure_oidc_provider() {
# caller must capture and use/display it now.
# OIDC_AUTHELIA_DOMAIN this Authelia instance's apex domain, for
# building discovery/authorization/token URLs.
# Returns 1 on failure (Authelia not installed, client ID already taken,
# secret generation failed) with the reason already logged.
# Returns 1 on failure (Authelia not installed, domain undeterminable,
# secret generation failed) with the reason already logged. A client_id
# that's already registered is NOT a failure — it gets replaced (see the
# comment at that check below).
_authelia_provision_oidc_client() {
local APP_NAME="$1" CLIENT_ID="$2" AUTH_POLICY="$3" RESTART_AUTH="$4"; shift 4
local -a REDIRECT_URIS=("$@")
@@ -1239,9 +1579,19 @@ _authelia_provision_oidc_client() {
return 1
fi
# A stale registration (e.g. from the interactive "Register an app" menu
# run previously without ever finishing — its plaintext secret was shown
# once and is gone, so the registration is dead weight either way) would
# otherwise permanently block this exact service's automated SSO offer
# with nothing but a warning. Confirmed live: this is what happened to
# ActualBudget the first time its own offer ran, against a client_id the
# menu had already registered in an earlier session. Safe to just
# replace — every automated caller here uses a fixed, service-specific
# client_id, so a collision means "this same service, already
# registered" rather than someone else's app using the same ID.
if grep -qF "client_id: '${CLIENT_ID}'" "$CONFIG_FILE" 2>/dev/null; then
log_warning "A client with ID '$CLIENT_ID' is already registered in $CONFIG_FILE."
return 1
log_warning "A client with ID '$CLIENT_ID' is already registered — replacing it with a fresh one (its old secret was never recoverable anyway)."
_authelia_remove_oidc_client "$CONFIG_FILE" "$CLIENT_ID"
fi
log_info "Generating client secret..."
+2
View File
@@ -236,6 +236,8 @@ _gitea_offer_authelia_sso() {
log_warning " Discovery URL: $_discovery_url"
log_warning " (The Client Secret above is shown once — it isn't stored in plaintext anywhere.)"
fi
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "gitea" "$GITEA_OIDC_DOMAIN"
}
# Offers to enable Gitea Actions (Gitea's own CI, largely GitHub-Actions-
+81
View File
@@ -198,6 +198,83 @@ fi
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() {
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 " - Default login: changeme@email.com / MyPassword (change immediately)"
echo " - Offer a Caddy reverse proxy and to start the container"
echo " - Offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
return 0
fi
@@ -259,6 +337,7 @@ install_mealie() {
( cd "$MEALIE_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Mealie image refreshed" \
|| 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
;;
cancel)
@@ -352,6 +431,8 @@ MEALIE_ENV
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
# Mealie${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}